/*
 * Copyright (c) 2011, 2012, Axel Nennker - http://axel.nennker.de/
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the names jsoncrypto, xmldap, xmldap.org, xmldap.com nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.jsoncrypto;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.sec.SECNamedCurves;
import org.bouncycastle.asn1.sec.SECObjectIdentifiers;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.agreement.ECDHBasicAgreement;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.digests.SHA384Digest;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.KDFParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.jcajce.provider.asymmetric.ec.ECUtil;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.BigIntegers;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoncrypto.crypto.CryptoUtils;
import org.jsoncrypto.crypto.KDFConcatGenerator;
import org.jsoncrypto.exceptions.CryptoException;
import org.jsoncrypto.util.Base64;
import org.jsoncrypto.util.Compress;

public class JcBase {

  public static final String SIGN_ALG_HS256 = "HS256";
  public static final String SIGN_ALG_HS384 = "HS384";
  public static final String SIGN_ALG_HS512 = "HS512";

  public static final String SIGN_ALG_ES256 = "ES256";
  public static final String SIGN_ALG_ES383 = "ES384";
  public static final String SIGN_ALG_ES512 = "ES512";

  public static final String SIGN_ALG_RS256 = "RS256";
  public static final String SIGN_ALG_RS383 = "RS384";
  public static final String SIGN_ALG_RS512 = "RS512";

  public static final String KW_ALG_DIR = "dir";
  public static final String KW_ALG_RSA1_5 = "RSA1_5";
  public static final String KW_ALG_RSA_OAEP = "RSA-OAEP";
  public static final String KW_ALG_ECDH_ES = "ECDH-ES";
  public static final String KW_ALG_ECDH_ES_KW128 = "ECDH-ES+A128KW";
  public static final String KW_ALG_ECDH_ES_KW256 = "ECDH-ES+A256KW";
  public static final String KW_ALG_A128KW = "A128KW";
  public static final String KW_ALG_A256KW = "A256KW";

  public static final String ENC_ALG_A128CBCHS256 = "A128CBC+HS256";
  public static final String ENC_ALG_A192CBCHS256 = "A192CBC+HS256";
  public static final String ENC_ALG_A256CBCHS256 = "A256CBC+HS256";
  public static final String ENC_ALG_A512CBCHS256 = "A512CBC+HS256";
  public static final String ENC_ALG_A128CBCHS512 = "A128CBC+HS512";
  public static final String ENC_ALG_A192CBCHS512 = "A192CBC+HS512";
  public static final String ENC_ALG_A256CBCHS512 = "A256CBC+HS512";
  public static final String ENC_ALG_A512CBCHS512 = "A512CBC+HS512";
  public static final String ENC_ALG_A128GCM = "A128GCM";
  public static final String ENC_ALG_A192GCM = "A192GCM";
  public static final String ENC_ALG_A256GCM = "A256GCM";
  public static final String ENC_ALG_A512GCM = "A512GCM";

  public static final Set<String> sSupportedEncAlgs;
  static {
    Set<String> aSet = new HashSet<String>();
    aSet.add(ENC_ALG_A128CBCHS256);
    aSet.add(ENC_ALG_A192CBCHS256);
    aSet.add(ENC_ALG_A256CBCHS256);
    aSet.add(ENC_ALG_A512CBCHS256);
    aSet.add(ENC_ALG_A128CBCHS512);
    aSet.add(ENC_ALG_A192CBCHS512);
    aSet.add(ENC_ALG_A256CBCHS512);
    aSet.add(ENC_ALG_A512CBCHS512);
    aSet.add(ENC_ALG_A128GCM);
    aSet.add(ENC_ALG_A192GCM);
    aSet.add(ENC_ALG_A256GCM);
    aSet.add(ENC_ALG_A512GCM);
    sSupportedEncAlgs = Collections.unmodifiableSet(aSet);
  }

  private final static Set<String> sCbcEncAlgorithms;
  static {
    Set<String> aSet = new HashSet<String>();
    aSet.add(ENC_ALG_A128CBCHS256);
    aSet.add(ENC_ALG_A192CBCHS256);
    aSet.add(ENC_ALG_A256CBCHS256);
    aSet.add(ENC_ALG_A512CBCHS256);
    aSet.add(ENC_ALG_A128CBCHS512);
    aSet.add(ENC_ALG_A192CBCHS512);
    aSet.add(ENC_ALG_A256CBCHS512);
    aSet.add(ENC_ALG_A512CBCHS512);
    sCbcEncAlgorithms = Collections.unmodifiableSet(aSet);
  }

  private final static Set<String> sGcmEncAlgorithms;
  static {
    Set<String> aSet = new HashSet<String>();
    aSet.add(ENC_ALG_A128GCM);
    aSet.add(ENC_ALG_A192GCM);
    aSet.add(ENC_ALG_A256GCM);
    aSet.add(ENC_ALG_A512GCM);
    sGcmEncAlgorithms = Collections.unmodifiableSet(aSet);
  }

  public static final Map<String, Integer> sEncAlgKeylengthMap;
  static {
    HashMap<String, Integer> aMap = new HashMap<String, Integer>();
    aMap.put(ENC_ALG_A128CBCHS256, 128);
    aMap.put(ENC_ALG_A192CBCHS256, 192);
    aMap.put(ENC_ALG_A256CBCHS256, 256);
    aMap.put(ENC_ALG_A512CBCHS256, 512);
    aMap.put(ENC_ALG_A128CBCHS512, 128);
    aMap.put(ENC_ALG_A192CBCHS512, 192);
    aMap.put(ENC_ALG_A256CBCHS512, 256);
    aMap.put(ENC_ALG_A512CBCHS512, 512);
    aMap.put(ENC_ALG_A128GCM, 128);
    aMap.put(ENC_ALG_A192GCM, 192);
    aMap.put(ENC_ALG_A256GCM, 256);
    aMap.put(ENC_ALG_A512GCM, 512);
    sEncAlgKeylengthMap = Collections.unmodifiableMap(aMap);
  }

  public static final Map<String, Integer> sIntAlgKeylengthMap;
  static {
    HashMap<String, Integer> aMap = new HashMap<String, Integer>();
    aMap.put(ENC_ALG_A128CBCHS256, 256);
    aMap.put(ENC_ALG_A192CBCHS256, 256);
    aMap.put(ENC_ALG_A256CBCHS256, 256);
    aMap.put(ENC_ALG_A512CBCHS256, 256);
    aMap.put(ENC_ALG_A128CBCHS512, 512);
    aMap.put(ENC_ALG_A192CBCHS512, 512);
    aMap.put(ENC_ALG_A256CBCHS512, 512);
    aMap.put(ENC_ALG_A512CBCHS512, 512);
    aMap.put(ENC_ALG_A128GCM, 128);
    aMap.put(ENC_ALG_A192GCM, 192);
    aMap.put(ENC_ALG_A256GCM, 256);
    aMap.put(ENC_ALG_A512GCM, 512);
    sIntAlgKeylengthMap = Collections.unmodifiableMap(aMap);
  }

  private JcBase() throws JSONException {
  }

  public static int getEncKeyLength(String jwtEncStr) throws JcBaseException {
    Integer keyLength = sEncAlgKeylengthMap.get(jwtEncStr);
    if (keyLength == null) {
      throw new JcBaseException("No such algorithm getEncKeyLength: enc=" + jwtEncStr);
    }
    return keyLength.intValue();
  }

  static int getIntKeyLength(String jwtIntStr) throws JcBaseException {
    Integer keyLength = sIntAlgKeylengthMap.get(jwtIntStr);
    if (keyLength == null) {
      throw new JcBaseException("No such algorithm getIntKeyLength: enc=" + jwtIntStr);
    }
    return keyLength.intValue();
  }

  static int getLargerKeylength(JSONObject jweHeader) throws JcBaseException {
    try {
      String jwtEncStr = jweHeader.getString("enc");
      return Math.max(getEncKeyLength(jwtEncStr), getIntKeyLength(jwtEncStr));
    } catch (Exception e) {
      throw new JcBaseException(e);
    }
  }

  /**
   * @param jwt
   *          alg = RS256 or RS384 or RS512
   * @param pubkey
   *          the siner's public RSA key
   * @return
   * @throws JcBaseException
   */
  static public boolean verify(String jwt, RSAPublicKey pubkey) throws JcBaseException {
    try {
      String jwtHeaderSegmentB64;
      String jwtPayloadSegment;
      String jwtCryptoSegment;
      String[] split = jwt.split("\\.");
      jwtHeaderSegmentB64 = split[0];
      jwtPayloadSegment = split[1];
      jwtCryptoSegment = split[2];

      String algorithm;
      JSONObject header = new JSONObject(new String(Base64.decodeUrl(jwtHeaderSegmentB64)));
      checkHeader(header);

      String jwtAlgStr = (String) header.get("alg");
      if ("RS256".equals(jwtAlgStr)) {
        algorithm = "SHA256withRSA";
      } else if ("RS384".equals(jwtAlgStr)) {
        algorithm = "SHA384withRSA";
      } else if ("RS512".equals(jwtAlgStr)) {
        algorithm = "SHA512withRSA";
      } else {
        throw new NoSuchAlgorithmException("JWT algorithm: " + jwtAlgStr);
      }

      String stringToSign = jwtHeaderSegmentB64 + "." + jwtPayloadSegment;
      Signature signature = Signature.getInstance(algorithm);
      signature.initVerify(pubkey);
      signature.update(stringToSign.getBytes("utf-8"));

      byte[] signatureBytes = Base64.decodeUrl(jwtCryptoSegment);

      return signature.verify(signatureBytes);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    } catch (SignatureException e) {
      throw new JcBaseException(e);
    } catch (Exception e) {
      throw new JcBaseException(e);
    }
  }

  static void checkJws(String jws) throws JcBaseException {
    String[] split = jws.split("\\.");
    if (split.length != 3) {
      throw new JcBaseException("jws does not have three segments");
    }
  }

  /**
   * @param jwt
   *          alg = ES256 or ES384 or ES512
   * @param x
   * @param y
   * @return
   * @throws JcBaseException
   */
  static boolean verify(String jws, byte[] secretKeyBytes) throws JcBaseException {
    try {
      checkJws(jws);
      String[] split = jws.split("\\.");
      byte[] headerBytes = Base64.decodeUrl(split[0]);
      byte[] payloadBytes = Base64.decodeUrl(split[1]);
      String signed = JcBase.sign(payloadBytes, new String(headerBytes), secretKeyBytes);
      return signed.equals(jws);
    } catch (Exception e) {
      throw new JcBaseException(e);
    }
  }

  /**
   * Returns a byte array representation of the specified big integer 
   * without the sign bit.
   * 
   * @param bigInt The big integer to be converted. Must not be
   *               {@code null}.
   *
   * @return A byte array representation of the big integer, without the
   *         sign bit.
   */
  static byte[] toBytesUnsigned(final BigInteger bigInt) {

    // Copied from Apache Commons Codec 1.8

    int bitlen = bigInt.bitLength();

    // round bitlen
    bitlen = ((bitlen + 7) >> 3) << 3;
    final byte[] bigBytes = bigInt.toByteArray();

    if (((bigInt.bitLength() % 8) != 0) && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) {
    
      return bigBytes;
    
    }

    // set up params for copying everything but sign bit
    int startSrc = 0;
    int len = bigBytes.length;

    // if bigInt is exactly byte-aligned, just skip signbit in copy
    if ((bigInt.bitLength() % 8) == 0) {
      
      startSrc = 1;
      len--;
    }
    
    final int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec
    final byte[] resizedBytes = new byte[bitlen / 8];
    System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len);
    return resizedBytes;
  }


  /**
   * Converts the specified big integers to byte arrays and returns their
   * array concatenation.
   *
   * @param r                 The R parameter. Must not be {@code null}.
   * @param s                 The S parameter. Must not be {@code null}.
   * @param rsByteArrayLength The expected concatenated array length.
   *
   * @return The resulting concatenated array.
   */
  private static byte[] formatSignature(final BigInteger r, 
                                  final BigInteger s,
                                  final int rsByteArrayLength) {

    byte[] rBytes = JcBase.toBytesUnsigned(r);
    byte[] sBytes = JcBase.toBytesUnsigned(s);

    final int outLength = rBytes.length + sBytes.length;

    byte[] rsBytes = new byte[rsByteArrayLength];

    int i = 0;

    // Copy R bytes to first array half, zero pad front
    int offset = (rsByteArrayLength / 2) - rBytes.length;

    i += offset;

    for (byte rB: rBytes) {

      rsBytes[i++] = rB;
    }

    // Copy S bytes to second array half, zero pad front
    i = rsByteArrayLength / 2;

    offset = (rsByteArrayLength / 2) - sBytes.length;

    i += offset;

    for (byte sB: sBytes) {

      rsBytes[i++] = sB;
    }

    return rsBytes;
  }

  /**
   * @param jwt
   *          alg = ES256 or ES384 or ES512
   * @param x
   * @param y
   * @return
   * @throws JcBaseException
   */
  static public boolean verify(String jwt, byte[] x, byte[] y) throws JcBaseException {
    try {
      String jwtHeaderSegment;
      String jwtPayloadSegment;
      String jwtCryptoSegment;
      String[] split = jwt.split("\\.");
      jwtHeaderSegment = split[0];
      jwtPayloadSegment = split[1];
      jwtCryptoSegment = split[2];

      byte[] signatureBytes = Base64.decodeUrl(jwtCryptoSegment);
      int len = signatureBytes.length / 2;
      byte[] rBytes = new byte[len];
      System.arraycopy(signatureBytes, 0, rBytes, 0, len);
      byte[] sBytes = new byte[len];
      System.arraycopy(signatureBytes, len, sBytes, 0, len);

      BigInteger r = new BigInteger(1, rBytes);
      BigInteger s = new BigInteger(1, sBytes);

      ASN1ObjectIdentifier oid;
      Digest digest;

      String header = new String(Base64.decodeUrl(jwtHeaderSegment));
      JSONObject headerO = new JSONObject(header);
      checkHeader(headerO);

      String jwtAlgStr = (String) headerO.get("alg");
      if ("ES256".equals(jwtAlgStr)) {
        oid = SECObjectIdentifiers.secp256r1;
        digest = new SHA256Digest();
      } else if ("ES384".equals(jwtAlgStr)) {
        oid = SECObjectIdentifiers.secp384r1;
        digest = new SHA384Digest();
      } else if ("ES512".equals(jwtAlgStr)) {
        oid = SECObjectIdentifiers.secp521r1;
        digest = new SHA512Digest();
      } else {
        throw new NoSuchAlgorithmException("JWT algorithm: " + jwtAlgStr);
      }

      X9ECParameters x9ECParameters = SECNamedCurves.getByOID(oid);

      ECDSASigner verifier = new ECDSASigner();
      BigInteger xB = new BigInteger(1, x);
      BigInteger yB = new BigInteger(1, y);
      ECCurve curve = x9ECParameters.getCurve();
      ECPoint qB = curve.createPoint(xB, yB, false);
      ECPoint q = new ECPoint.Fp(curve, qB.getX(), qB.getY());
      ECDomainParameters ecDomainParameters = new ECDomainParameters(curve, x9ECParameters.getG(),
          x9ECParameters.getN(), x9ECParameters.getH(), x9ECParameters.getSeed());
      ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(q, ecDomainParameters);
      verifier.init(false, ecPublicKeyParameters);
      String hp = jwtHeaderSegment + "." + jwtPayloadSegment;
      byte[] bytes = hp.getBytes("utf-8");
      digest.update(bytes, 0, bytes.length);
      byte[] out = new byte[digest.getDigestSize()];
      /* int result = */digest.doFinal(out, 0);

      boolean verified = verifier.verifySignature(out, r, s);
      return verified;
    } catch (JSONException e) {
      throw new JcBaseException(e);
    } catch (UnsupportedEncodingException e) {
      throw new JcBaseException(e);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    } catch (Exception e) {
      throw new JcBaseException(e);
    }
  }

  /**
   * @param contentToSign
   * @param jwsHeader
   *          alg = ES256 or ES384 or ES512
   * @param D
   *          EC private key value D
   * @return jws
   * @throws JcBaseException
   */
  public static String sign(byte[] contentToSign, JSONObject jwsHeader, BigInteger D) throws JcBaseException {
    try {
      checkHeader(jwsHeader);
      ASN1ObjectIdentifier oid;
      Digest digest;
      int signatureLength;
      String jwtAlgStr = jwsHeader.getString("alg");
      if ("ES256".equals(jwtAlgStr)) {
        oid = SECObjectIdentifiers.secp256r1;
        digest = new SHA256Digest();
        signatureLength = 64;
      } else if ("ES384".equals(jwtAlgStr)) {
        oid = SECObjectIdentifiers.secp384r1;
        digest = new SHA384Digest();
        signatureLength = 96;
      } else if ("ES512".equals(jwtAlgStr)) {
        oid = SECObjectIdentifiers.secp521r1;
        digest = new SHA512Digest();
        signatureLength = 132;
      } else {
        throw new NoSuchAlgorithmException("JWT algorithm: " + jwtAlgStr);
      }

      X9ECParameters x9ECParameters = SECNamedCurves.getByOID(oid);
      ECDomainParameters ecParameterSpec = new ECDomainParameters(x9ECParameters.getCurve(), x9ECParameters.getG(),
          x9ECParameters.getN(), x9ECParameters.getH(), x9ECParameters.getSeed());
      ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(D, ecParameterSpec);

      String b64 = Base64.encodeBytes(jwsHeader.toString().getBytes("utf-8"),
          org.jsoncrypto.util.Base64.DONT_BREAK_LINES | org.jsoncrypto.util.Base64.URL);
      StringBuffer sb = new StringBuffer(b64);
      sb.append('.');
      b64 = Base64.encodeBytes(contentToSign, org.jsoncrypto.util.Base64.DONT_BREAK_LINES
          | org.jsoncrypto.util.Base64.URL);
      sb.append(b64);

      String stringToSign = sb.toString();
      byte[] bytes = stringToSign.getBytes("utf-8");
      digest.update(bytes, 0, bytes.length);
      byte[] out = new byte[digest.getDigestSize()];
      /* int result = */digest.doFinal(out, 0);

      sb.append('.');

      String signed = signECDSA(ecPrivateKeyParameters, out, signatureLength);

      sb.append(signed);
      return sb.toString();
    } catch (JSONException e) {
      throw new JcBaseException(e);
    } catch (UnsupportedEncodingException e) {
      throw new JcBaseException(e);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    }
  }

  private static String signECDSA(ECPrivateKeyParameters ecPrivateKeyParameters, byte[] bytes, int rsByteArrayLength)
      throws UnsupportedEncodingException {
    ECDSASigner signer = new ECDSASigner();
    signer.init(true, ecPrivateKeyParameters);
    BigInteger[] res = signer.generateSignature(bytes);
    BigInteger r = res[0];
    BigInteger s = res[1];

    byte[] rsBytes = JcBase.formatSignature(r, s, rsByteArrayLength);
    String signed = Base64.encodeBytes(rsBytes, org.jsoncrypto.util.Base64.DONT_BREAK_LINES | org.jsoncrypto.util.Base64.URL);
    // System.out.println("Signed:" + signed);
    return signed;
  }

//  // TODO FIXME let hashByteLen be a third parameter to replace the fixed 32
//  private static String rs2jwt(BigInteger r, BigInteger s) {
//    // System.out.println("R:" + r.toString());
//    // System.out.println("S:" + s.toString());
//    byte[] rBytes = r.toByteArray();
//    // System.out.println("rBytes.length:" + rBytes.length);
//    byte[] sBytes = s.toByteArray();
//    // System.out.println("sBytes.length:" + sBytes.length);
//    // StringBuffer sb = new StringBuffer();
//    // for (int i=0; i<rBytes.length;i++) {
//    // sb.append(String.valueOf((int)rBytes[i]));
//    // sb.append(',');
//    // }
//    // System.out.println("Rbytes:" + sb.toString());
//    // sb = new StringBuffer();
//    // for (int i=0; i<sBytes.length;i++) {
//    // sb.append(String.valueOf((int)sBytes[i]));
//    // sb.append(',');
//    // }
//    // System.out.println("Sbytes:" + sb.toString());
//    byte[] rsBytes = new byte[64];
//    for (int i = 0; i < rsBytes.length; i++) {
//      rsBytes[i] = 0;
//    }
//    if (rBytes.length >= 32) {
//      System.arraycopy(rBytes, rBytes.length - 32, rsBytes, 0, 32);
//    } else {
//      System.arraycopy(rBytes, 0, rsBytes, 32 - rBytes.length, rBytes.length);
//    }
//    if (sBytes.length >= 32) {
//      System.arraycopy(sBytes, sBytes.length - 32, rsBytes, 32, 32);
//    } else {
//      System.arraycopy(sBytes, 0, rsBytes, 64 - sBytes.length, sBytes.length);
//    }
//    String signed = Base64.encodeBytes(rsBytes, org.jsoncrypto.util.Base64.DONT_BREAK_LINES
//        | org.jsoncrypto.util.Base64.URL);
//    return signed;
//  }

  /**
   * @param contentToSign
   * @param jwsHeaderStr
   *          alg = RS256 or RS 384 or RS512
   * @param privateKey
   *          my private RSA key
   * @return
   * @throws JcBaseException
   */
  public static String sign(byte[] contentToSign, String jwsHeaderStr, RSAPrivateKey privateKey) throws JcBaseException {
    try {
      String b64 = Base64.encodeBytes(jwsHeaderStr.getBytes("utf-8"), org.jsoncrypto.util.Base64.DONT_BREAK_LINES
          | org.jsoncrypto.util.Base64.URL);
      StringBuffer sb = new StringBuffer(b64);
      sb.append('.');
      b64 = Base64.encodeBytes(contentToSign, org.jsoncrypto.util.Base64.DONT_BREAK_LINES
          | org.jsoncrypto.util.Base64.URL);
      sb.append(b64);

      String stringToSign = sb.toString();

      sb.append('.');

      JSONObject jwsHeader = new JSONObject(jwsHeaderStr);
      checkHeader(jwsHeader);

      String jwtAlgStr = jwsHeader.getString("alg");
      Signature signature;
      String algorithm;
      if ("RS256".equals(jwtAlgStr)) {
        algorithm = "SHA256withRSA";
      } else if ("RS384".equals(jwtAlgStr)) {
        algorithm = "SHA384withRSA";
      } else if ("RS512".equals(jwtAlgStr)) {
        algorithm = "SHA512withRSA";
      } else {
        throw new NoSuchAlgorithmException("JWT algorithm: " + jwtAlgStr);
      }
      signature = Signature.getInstance(algorithm);
      signature.initSign(privateKey);
      signature.update(stringToSign.getBytes("utf-8"));
      byte[] bytes = signature.sign();

      b64 = Base64.encodeBytes(bytes, org.jsoncrypto.util.Base64.DONT_BREAK_LINES | org.jsoncrypto.util.Base64.URL);
      sb.append(b64);
      return sb.toString();
    } catch (JSONException e) {
      throw new JcBaseException(e);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    } catch (InvalidKeyException e) {
      throw new JcBaseException(e);
    } catch (SignatureException e) {
      throw new JcBaseException(e);
    } catch (UnsupportedEncodingException e) {
      throw new JcBaseException(e);
    }
  }

  public static Mac getMac(String macAlgorithmName) throws JcBaseException {
    String jceName;
    if (SIGN_ALG_HS256.equals(macAlgorithmName)) { // HMAC SHA-256
      jceName = "HMACSHA256";
    } else if (SIGN_ALG_HS384.equals(macAlgorithmName)) { // HMAC SHA-384
      jceName = "HMACSHA384";
    } else if (SIGN_ALG_HS512.equals(macAlgorithmName)) { // HMAC SHA-512
      jceName = "HMACSHA512";
    } else {
      // xmldsig names don not work here but JCA/JCE name do
      jceName = macAlgorithmName;
    }
    try {
      Mac mac = Mac.getInstance(jceName);
      return mac;
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    }
  }

  static byte[] doMac(Mac mac, byte[] passphraseBytes, byte[] bytes) throws JcBaseException {
    try {
      mac.init(new SecretKeySpec(passphraseBytes, mac.getAlgorithm()));
      mac.update(bytes);
      return mac.doFinal();
    } catch (InvalidKeyException e) {
      throw new JcBaseException(e);
    }
  }

  /**
   * @param contentToSign
   * @param jwsHeaderStr
   *          alg = HS256 or HS384 or HS512
   * @param passphraseBytes
   * @return
   * @throws JcBaseException
   */
  public static String sign(byte[] contentToSign, String jwsHeaderStr, byte[] passphraseBytes) throws JcBaseException {
    try {
      String b64 = Base64.encodeBytes(jwsHeaderStr.getBytes(), org.jsoncrypto.util.Base64.DONT_BREAK_LINES
          | org.jsoncrypto.util.Base64.URL);
      StringBuffer sb = new StringBuffer(b64);
      sb.append('.');
      b64 = Base64.encodeBytes(contentToSign, org.jsoncrypto.util.Base64.DONT_BREAK_LINES
          | org.jsoncrypto.util.Base64.URL);
      sb.append(b64);

      String stringToSign = sb.toString();

      sb.append('.');
      String signed;

      JSONObject jwsHeader = new JSONObject(jwsHeaderStr);
      checkHeader(jwsHeader);

      String jwtAlgStr = jwsHeader.getString("alg");
      Mac mac = getMac(jwtAlgStr);
      byte[] bytes = doMac(mac, passphraseBytes, stringToSign.getBytes());
      signed = Base64.encodeBytes(bytes, org.jsoncrypto.util.Base64.DONT_BREAK_LINES | org.jsoncrypto.util.Base64.URL);
      sb.append(signed);
      return sb.toString();
    } catch (JSONException e) {
      throw new JcBaseException(e);
    }
  }

  /**
   * @param contentBytes
   * @param jweHeaderStr
   *          alg = ENC_ALG_A128KW or ENC_ALG_A256KW
   * @param key
   *          symmetric key
   * @return jwe
   * @throws JcBaseException
   */
  public static String encrypt(byte[] contentBytes, String jweHeaderStr, SecretKey key, SecureRandom secureRandom)
      throws JcBaseException {
    try {
      JSONObject jweHeader = new JSONObject(jweHeaderStr);
      checkHeader(jweHeader);
      String alg = jweHeader.getString("alg");
      if (!(KW_ALG_A128KW.equals(alg)) && !(KW_ALG_A256KW.equals(alg))) {
        throw new JcBaseException("alg not supported: " + alg);
      }

      int keyLengthInBits = JcBase.getLargerKeylength(jweHeader);
      SecretKey contentEncryptionKey = generateRandomContentEncryptionKey(keyLengthInBits, secureRandom);
      // byte[] cmk = new byte[keyLengthInBits / 8];
      // sr.nextBytes(cmk);
      // SecretKeySpec contentEncryptionKey = new SecretKeySpec(cmk,
      // "AES");

      // AES key wrap
      String encodedJweEncryptedKey = generateEncodedEncryptedKeyAesKW(contentEncryptionKey, key);

      byte[] iv = new byte[16];
      // bits
      secureRandom.nextBytes(iv);
      String encodedIv = Base64.encodeBytes(iv, org.jsoncrypto.util.Base64.DONT_BREAK_LINES
          | org.jsoncrypto.util.Base64.URL);

      String jwtEncStr = jweHeader.getString("enc");
      return encryptAes(contentBytes, jweHeaderStr, jwtEncStr, contentEncryptionKey, encodedJweEncryptedKey, encodedIv);
    } catch (JSONException e) {
      throw new JcBaseException(e);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    }
  }

  public static String generateEncodedEncryptedKeyAesKW(SecretKey contentEncryptionKey, SecretKey contentMasterKey)
      throws JcBaseException {
    try {
      byte[] wrappedKey = CryptoUtils.wrapAesKey(contentEncryptionKey, contentMasterKey);
      String encodedJweEncryptedKey = Base64.encodeBytes(wrappedKey, org.jsoncrypto.util.Base64.DONT_BREAK_LINES
          | org.jsoncrypto.util.Base64.URL);
      return encodedJweEncryptedKey;
    } catch (BadPaddingException e) {
      throw new JcBaseException(e);
    } catch (InvalidKeyException e) {
      throw new JcBaseException(e);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    } catch (NoSuchPaddingException e) {
      throw new JcBaseException(e);
    } catch (IllegalBlockSizeException e) {
      throw new JcBaseException(e);
    }
  }

  private static String encryptAes(byte[] contentBytes, String jweHeaderStr, String jwtEncStr,
      SecretKey contentEncryptionKey, String encodedJweEncryptedKey, String encodedIv) throws JcBaseException,
      NoSuchAlgorithmException {
    if (sCbcEncAlgorithms.contains(jwtEncStr)) {
      return encryptAesCbc(contentBytes, jweHeaderStr, contentEncryptionKey, jwtEncStr, encodedJweEncryptedKey,
          encodedIv);
    } else if (sGcmEncAlgorithms.contains(jwtEncStr)) {
      return encryptAesGcm(contentBytes, jweHeaderStr, contentEncryptionKey, encodedJweEncryptedKey, encodedIv);
    } else {
      throw new NoSuchAlgorithmException("WebToken RSA encrypt: enc=" + jwtEncStr);
    }
  }

  public static void checkHeader(JSONObject jweHeader) throws JcBaseException {
    @SuppressWarnings("rawtypes")
    Iterator iter = jweHeader.keys();
    while (iter.hasNext()) {
      String key = (String) iter.next();
      if ("alg".equals(key))
        continue;
      if ("enc".equals(key))
        continue;
      if ("int".equals(key))
        continue;
      if ("kdf".equals(key))
        continue;
      if ("iv".equals(key))
        continue;
      if ("epk".equals(key))
        continue;
      if ("zip".equals(key))
        continue;
      if ("jku".equals(key))
        continue;
      if ("jwk".equals(key))
        continue;
      if ("x5u".equals(key))
        continue;
      if ("x5t".equals(key))
        continue;
      if ("x5c".equals(key))
        continue;
      if ("kid".equals(key))
        continue;
      if ("typ".equals(key))
        continue;
      if ("cty".equals(key))
        continue;
      if ("jku".equals(key))
        continue;
      if ("jwk".equals(key))
        continue;
      throw new JcBaseException("unknown key in header: " + key);
    }
  }

  /**
   * @param encrypted
   *          alg = ENC_ALG_A128KW or ENC_ALG_A256KW
   * @param key
   *          symmetric key
   * @return
   * @throws JcBaseException
   */
  public static byte[] decrypt(String encrypted, SecretKey key) throws JcBaseException {
    try {
      String[] split = encrypted.split("\\.");
      String encodedJwtHeaderSegment = split[0];
      String encodedJwtKeySegment = split[1];
      String encodedJwtIvSegment = split[2];
      String encodedJwtCryptoSegment = split[3];
      String encodedjwtIntegritySegment = split[4];
      byte[] jwtIntegritySegmentBytes = Base64.decodeUrl(encodedjwtIntegritySegment);

      String jwtHeaderSegment = new String(Base64.decodeUrl(encodedJwtHeaderSegment));

      JSONObject header = new JSONObject(jwtHeaderSegment);
      checkHeader(header);

      String jwtAlgStr = (String) header.get("alg");
      byte[] secretKeyBytes = unwrapKey(key, encodedJwtKeySegment, jwtAlgStr);

      return doDecrypt(encodedJwtHeaderSegment, encodedJwtKeySegment, encodedJwtCryptoSegment,
          jwtIntegritySegmentBytes, header, secretKeyBytes, encodedJwtIvSegment);
    } catch (JSONException e) {
      throw new JcBaseException(e);
    } catch (InvalidKeyException e) {
      throw new JcBaseException(e);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    } catch (NoSuchPaddingException e) {
      throw new JcBaseException(e);
    } catch (IllegalBlockSizeException e) {
      throw new JcBaseException(e);
    } catch (BadPaddingException e) {
      throw new JcBaseException(e);
    } catch (InvalidCipherTextException e) {
      throw new JcBaseException(e);
    } catch (CryptoException e) {
      throw new JcBaseException(e);
    } catch (Exception e) {
      throw new JcBaseException(e);
    }
  }

  static byte[] unwrapKey(SecretKey key, String encodedJwtKeySegment, String jwtAlgStr) throws Exception,
      NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException,
      BadPaddingException, JcBaseException {
    byte[] secretKeyBytes;
    if ((KW_ALG_A128KW.equals(jwtAlgStr)) || (KW_ALG_A256KW.equals(jwtAlgStr))) {
      byte[] keyToBeUnwrapped = Base64.decodeUrl(encodedJwtKeySegment);
      Key cmk = CryptoUtils.unwrapAesKey(keyToBeUnwrapped, key);
      secretKeyBytes = cmk.getEncoded();
    } else {
      throw new JcBaseException("alg not supported: " + jwtAlgStr);
    }
    return secretKeyBytes;
  }

  /**
   * @param encrypted
   *          alg = ENC_ALG_ECDH_ES
   * @param ecPublicKeyParameters
   *          sender's public key
   * @param ecPrivateKeyParameters
   *          my private key
   * @param kdfDigest
   * @return
   * @throws JcBaseException
   */
  public static byte[] decrypt(String encrypted, ECPublicKeyParameters ecPublicKeyParameters,
      ECPrivateKeyParameters ecPrivateKeyParameters, Digest kdfDigest) throws JcBaseException {
    try {
      String[] split = encrypted.split("\\.");
      String encodedJwtHeaderSegment = split[0];
      String encodedJwtKeySegment = split[1];
      String encodedJwtIvSegment = split[2];
      String encodedJwtCryptoSegment = split[3];
      String encodedJwtIntegritySegment = split[4];
      byte[] jwtIntegritySegmentBytes = Base64.decodeUrl(encodedJwtIntegritySegment);

      String jwtHeaderSegment = new String(Base64.decodeUrl(encodedJwtHeaderSegment));
      JSONObject header = new JSONObject(jwtHeaderSegment);
      checkHeader(header);

      String jwtAlgStr = (String) header.get("alg");
      String jwtEncStr = (String) header.get("enc");

      int keylength = 256;

      byte[] secretKeyBytes;
      if (KW_ALG_ECDH_ES.equals(jwtAlgStr)) {
        ECDHBasicAgreement ecdhBasicAgreement = new ECDHBasicAgreement();
        ecdhBasicAgreement.init(ecPrivateKeyParameters);
        BigInteger z = ecdhBasicAgreement.calculateAgreement(ecPublicKeyParameters);
        // System.out.println("ECDH-ES z=" + z.toString());
        byte[] zBytes = BigIntegers.asUnsignedByteArray(z);
        secretKeyBytes = generateCEK(zBytes, keylength / 8, kdfDigest, jwtEncStr);
      } else {
        throw new NoSuchAlgorithmException("JWT algorithm: " + jwtAlgStr);
      }

      return doDecrypt(encodedJwtHeaderSegment, encodedJwtKeySegment, encodedJwtCryptoSegment,
          jwtIntegritySegmentBytes, header, secretKeyBytes, encodedJwtIvSegment);
    } catch (JSONException e) {
      throw new JcBaseException(e);
    } catch (InvalidKeyException e) {
      throw new JcBaseException(e);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    } catch (InvalidCipherTextException e) {
      throw new JcBaseException(e);
    } catch (CryptoException e) {
      throw new JcBaseException(e);
    } catch (Exception e) {
      throw new JcBaseException(e);
    }
  }

  static byte[] doJwtDecrypt(String encodedJwtHeaderSegment, String encodedJwtKeySegment,
      String encodedJwtCryptoSegment, byte[] jwtIntegritySegmentBytes, JSONObject header, byte[] secretKeyBytes,
      String encodedJwtIvSegment) throws NoSuchAlgorithmException, NoSuchPaddingException, Exception,
      InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException,
      InvalidCipherTextException {
    String jwtEncStr = header.getString("enc");
    byte[] jwtCryptoSegmentBytes = Base64.decodeUrl(encodedJwtCryptoSegment);

    if (sCbcEncAlgorithms.contains(jwtEncStr)) {

      int cekLength = JcBase.enc2cekLength(header.getString("enc"));
      Digest kdfDigest = new SHA256Digest();
      byte[] cek = JcBase.generateCEK(secretKeyBytes, cekLength, kdfDigest, jwtEncStr);

      Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
      String ivStr = encodedJwtIvSegment;
      byte[] ivBytes = Base64.decodeUrl(ivStr);
      IvParameterSpec ivParameter = new IvParameterSpec(ivBytes);
      cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cek, "AES"), ivParameter);
      byte[] jwtCrytoSegmentBytes = Base64.decodeUrl(encodedJwtCryptoSegment);
      byte[] cleartextBytes = cipher.doFinal(jwtCrytoSegmentBytes);

      String jwtIntStr = getIntAlg(header);
      Mac mac = getMac(jwtIntStr);
      int cikLength = mac.getMacLength();
      byte[] cik = JcBase.generateCIK(secretKeyBytes, cikLength, kdfDigest, jwtEncStr);
      String stringToSign = encodedJwtHeaderSegment + "." + encodedJwtKeySegment + "." + encodedJwtIvSegment + "."
          + encodedJwtCryptoSegment;
      byte[] bytes = JcBase.doMac(mac, cik, stringToSign.getBytes());

      if (!org.bouncycastle.util.Arrays.constantTimeAreEqual(bytes, jwtIntegritySegmentBytes)) {
        throw new JcBaseException("integrity check failed");
      }

      return cleartextBytes;
    } else if (sGcmEncAlgorithms.contains(jwtEncStr)) {
      String ivB64 = encodedJwtIvSegment;
      byte[] ivBytes = Base64.decodeUrl(ivB64);
      KeyParameter key = new KeyParameter(secretKeyBytes);
      int macSizeBits = 128;

      byte[] nonce = ivBytes;
      String associatedText = encodedJwtHeaderSegment + "." + encodedJwtKeySegment + "." + encodedJwtIvSegment;
      AEADParameters aeadParameters = new AEADParameters(key, macSizeBits, nonce, associatedText.getBytes());
      SecretKeySpec keySpec = new SecretKeySpec(secretKeyBytes, "AES");
      return CryptoUtils.aesgcmDecrypt(aeadParameters, keySpec, jwtCryptoSegmentBytes, jwtIntegritySegmentBytes);
    } else {
      throw new NoSuchAlgorithmException("RSA AES decrypt " + jwtEncStr);
    }
  }

  static String getIntAlg(JSONObject header) throws JcBaseException {
    String alg = header.optString("int", null);
    if (alg == null) {
      String enc;
      try {
        enc = header.getString("enc");
      } catch (JSONException e) {
        throw new JcBaseException(e);
      }
      // A128CBC+HS256
      String split[] = enc.split("\\+");
      if (split.length < 2) {
        throw new JcBaseException("can not determine integrity algorithm " + header.toString());
      }
      return split[1];
    } else {
      return alg;
    }
  }

  static byte[] doDecrypt(String encodedJwtHeaderSegment, String encodedJwtKeySegment, String encodedJwtCryptoSegment,
      byte[] jwtIntegritySegmentBytes, JSONObject header, byte[] secretKeyBytes, String encodedJwtIvSegment)
      throws NoSuchAlgorithmException, Exception, CryptoException, InvalidKeyException, InvalidCipherTextException {
    String jwtEncStr = (String) header.get("enc");
    if (sCbcEncAlgorithms.contains(jwtEncStr)) {
      int cekLength = enc2cekLength(jwtEncStr);
      // FIXME other digest?
      Digest kdfDigest = new SHA256Digest();
      byte[] cek = generateCEK(secretKeyBytes, cekLength, kdfDigest, jwtEncStr);

      System.out.println("base64 CEK="
          + Base64.encodeBytes(cek, org.jsoncrypto.util.Base64.DONT_BREAK_LINES | org.jsoncrypto.util.Base64.URL));

      // byte[] jwtCryptoSegmentBytes =
      // Base64.decodeUrl(encodedJwtCryptoSegment);
      // byte[] cleartext =
      // CryptoUtils.decryptAESCBC(jwtCryptoSegmentBytes,
      // cek);
      Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
      byte[] ivBytes = Base64.decodeUrl(encodedJwtIvSegment);
      IvParameterSpec ivParameter = new IvParameterSpec(ivBytes);
      cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cek, "AES"), ivParameter);

      byte[] jwtCrytoSegmentBytes = Base64.decodeUrl(encodedJwtCryptoSegment);
      byte[] cryptobytes;
      String zip = header.optString("zip", null);
      if ("DEF".equals(zip)) {
        cryptobytes = Compress.decompress(jwtCrytoSegmentBytes);
      } else {
        if (zip != null) {
          throw new JcBaseException("value of zip header MUST be DEF");
        } else {
          cryptobytes = jwtCrytoSegmentBytes;
        }
      }

      byte[] cleartextBytes = cipher.doFinal(cryptobytes);

      String jwtIntStr = getIntAlg(header);
      Mac mac = getMac(jwtIntStr);
      int cikLength = mac.getMacLength();
      byte[] cik = generateCIK(secretKeyBytes, cikLength, kdfDigest, jwtEncStr);
      String stringToSign = encodedJwtHeaderSegment + "." + encodedJwtKeySegment + "." + encodedJwtIvSegment + "."
          + encodedJwtCryptoSegment;
      byte[] bytes = doMac(mac, cik, stringToSign.getBytes());
      if (Arrays.constantTimeAreEqual(bytes, jwtIntegritySegmentBytes)) {
        return cleartextBytes;
      } else {
        throw new JcBaseException("jwt integrity check failed");
      }
    } else if (sGcmEncAlgorithms.contains(jwtEncStr)) {
      String ivB64 = encodedJwtIvSegment;
      byte[] ivBytes = Base64.decodeUrl(ivB64);
      KeyParameter key = new KeyParameter(secretKeyBytes);
      int macSizeBits = 128;

      byte[] nonce = ivBytes;
      String associatedText = encodedJwtHeaderSegment + "." + encodedJwtKeySegment + "." + encodedJwtIvSegment;
      AEADParameters aeadParameters = new AEADParameters(key, macSizeBits, nonce, associatedText.getBytes());
      byte[] jwtCryptoSegmentBytes = Base64.decodeUrl(encodedJwtCryptoSegment);

      SecretKeySpec sks = new SecretKeySpec(secretKeyBytes, "AES");
      byte[] plaintext = CryptoUtils
          .aesgcmDecrypt(aeadParameters, sks, jwtCryptoSegmentBytes, jwtIntegritySegmentBytes);

      String zip = header.optString("zip", null);
      if ("DEF".equals(zip)) {
        return Compress.decompress(plaintext);
      } else {
        if (zip != null) {
          throw new JcBaseException("value of zip header MUST be DEF");
        } else {
          return plaintext;
        }
      }

    } else {
      throw new NoSuchAlgorithmException("RSA AES decrypt " + jwtEncStr);
    }
  }

  /**
   * @param contentBytes
   * @param jweHeaderStr
   *          alg = ENC_ALG_ECDH_ES
   * @param ecPublicKeyParameters
   *          recipient's public key
   * @param ecPrivateKeyParameters
   *          my private key
   * @param kdfDigest
   *          the Digest to use
   * @return
   * @throws JcBaseException
   */
  static String encrypt(byte[] contentBytes, String jweHeaderStr, ECPublicKeyParameters ecPublicKeyParameters,
      ECPrivateKeyParameters ecPrivateKeyParameters, String encodedIv) throws JcBaseException {
    try {
      SecretKey contentEncryptionKey;
      JSONObject jweHeader = new JSONObject(jweHeaderStr);
      checkHeader(jweHeader);

      String jwtEncStr = jweHeader.getString("enc");
      int keylength = getEncKeyLength(jwtEncStr);

      Digest kdfDigest = getKdfDigest(jweHeader);

      String encodedJweEncryptedKey;

      String jwtAlgStr = (String) jweHeader.get("alg");
      if (KW_ALG_ECDH_ES.equals(jwtAlgStr)) {
        ECDHBasicAgreement ecdhBasicAgreement = new ECDHBasicAgreement();
        ecdhBasicAgreement.init(ecPrivateKeyParameters);
        BigInteger z = ecdhBasicAgreement.calculateAgreement(ecPublicKeyParameters);
        // System.out.println("ECDH-ES z=" + z.toString());
        byte[] zBytes = BigIntegers.asUnsignedByteArray(z);
        byte[] secretKeyBytes = generateCEK(zBytes, keylength / 8, kdfDigest, jwtEncStr);
        contentEncryptionKey = new SecretKeySpec(secretKeyBytes, "RAW");
        // encrypt the content encryption key using secretKey
        encodedJweEncryptedKey = "";
      } else if (KW_ALG_ECDH_ES_KW128.equals(jwtAlgStr)) {
        // FIXME
        throw new RuntimeException("not implemented");
      } else if (KW_ALG_ECDH_ES_KW256.equals(jwtAlgStr)) {
        // FIXME
        throw new RuntimeException("not implemented");
      } else {
        throw new NoSuchAlgorithmException("JWT algorithm: " + jwtAlgStr);
      }

      return encryptAes(contentBytes, jweHeaderStr, jwtEncStr, contentEncryptionKey, encodedJweEncryptedKey, encodedIv);
    } catch (JSONException e) {
      throw new JcBaseException(e);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    } catch (Exception e) {
      throw new JcBaseException(e);
    }
  }

  static Digest getKdfDigest(JSONObject jweHeader) throws JcBaseException {
    try {
      Digest kdfDigest;
      String jwtKdfStr = jweHeader.optString("kdf", null);
      if (jwtKdfStr == null) {
        jwtKdfStr = "CS256";
      }
      kdfDigest = kdf2digest(jwtKdfStr);
      return kdfDigest;
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    }
  }

  static ASN1ObjectIdentifier getNamedCurveOid(String jwtCrvStr) {
    if ("P-256".equals(jwtCrvStr) || "secp256r1".equals(jwtCrvStr)) {
      return SECObjectIdentifiers.secp256r1;
    } else if ("P-384".equals(jwtCrvStr) || "secp384r1".equals(jwtCrvStr)) {
      return SECObjectIdentifiers.secp384r1;
    } else if ("P-128".equals(jwtCrvStr) || "secp128r1".equals(jwtCrvStr)) {
      return SECObjectIdentifiers.secp128r1;
    } else if ("P-521".equals(jwtCrvStr) || "secp521r1".equals(jwtCrvStr)) {
      return SECObjectIdentifiers.secp521r1;
    } else {
      return null;
    }
  }

  /**
   * @param encrypted
   *          alg = ECDH-ES
   * @param x
   *          sender's public key x value
   * @param y
   *          sender's public key y value
   * @param D
   *          my private key D value
   * @return clear text bytes
   * @throws JcBaseException
   */
  public static byte[] decrypt(String encrypted, ECPrivateKeyParameters ecPrivateKeyParameters) throws JcBaseException {
    try {
      String[] split = encrypted.split("\\.");
      String headerB64 = split[0];

      String jwtHeaderSegment = new String(Base64.decodeUrl(headerB64));

      JSONObject header = new JSONObject(jwtHeaderSegment);
      checkHeader(header);

      String jwtAlgStr = header.getString("alg");
      if (!"ECDH-ES".equals(jwtAlgStr)) {
        throw new NoSuchAlgorithmException("JWT algorithm: " + jwtAlgStr);
      }

      ECPublicKeyParameters ecPublicKeyParameters = getECPublicKeyParameters(header);

      // FIXME other digests ?
      Digest digest = new SHA256Digest();

      return decrypt(encrypted, ecPublicKeyParameters, ecPrivateKeyParameters, digest);
    } catch (JSONException e) {
      throw new JcBaseException(e);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    } catch (Exception e) {
      throw new JcBaseException(e);
    }
  }

  static ECPublicKeyParameters getecPublicKeyParameters(String crv, BigInteger x, BigInteger y) {
    ASN1ObjectIdentifier oid = SECNamedCurves.getOID(crv);
    X9ECParameters x9ECParameters = ECUtil.getNamedCurveByOid(oid);
    ECCurve curve = x9ECParameters.getCurve();
    ECPoint qB = curve.createPoint(x, y, false);
    ECPoint q = new ECPoint.Fp(curve, qB.getX(), qB.getY());
    ECDomainParameters ecDomainParameters = new ECDomainParameters(curve, x9ECParameters.getG(), x9ECParameters.getN(),
        x9ECParameters.getH(), x9ECParameters.getSeed());
    return new ECPublicKeyParameters(q, ecDomainParameters);

  }

  static ECPublicKeyParameters getECPublicKeyParameters(JSONObject header) throws JSONException, Exception,
      NoSuchAlgorithmException {
    JSONObject epkJson = header.getJSONObject("epk");
    JSONArray jwkJSON = epkJson.getJSONArray("jwk");
    JSONObject keyJSON = jwkJSON.getJSONObject(0);
    // FIXME should there be a default or should this fail if no crv is
    // specified
    String jwtCrvStr = keyJSON.optString("crv", "P-256");
    String encodedX = keyJSON.getString("x");
    String encodedY = keyJSON.getString("y");

    BigInteger x = new BigInteger(1, Base64.decodeUrl(encodedX));
    BigInteger y = new BigInteger(1, Base64.decodeUrl(encodedY));

    String curve;
    if ("P-256".equals(jwtCrvStr) || ("secp256r1".equals(jwtCrvStr))) {
      curve = "secp256r1";
    } else if ("P-384".equals(jwtCrvStr) || ("secp384r1".equals(jwtCrvStr))) {
      curve = "secp384r1";
    } else if ("P-521".equals(jwtCrvStr) || ("secp521r1".equals(jwtCrvStr))) {
      curve = "secp521r1";
    } else {
      throw new IllegalArgumentException("unsupported crv: " + jwtCrvStr);
    }

    return getecPublicKeyParameters(curve, x, y);
  }

  // /**
  // * @param contentBytes
  // * @param jweHeaderStr
  // * alg = ECDH-ES
  // * @param x
  // * recipients public key x
  // * @param y
  // * recipients public key y
  // * @param D
  // * my private key D
  // * @return the jwe
  // * @throws JcBaseException
  // */
  // public static String encrypt(
  // byte[] contentBytes,
  // String jweHeaderStr,
  // ECPrivateKeyParameters ecPrivateKeyParameters)
  // throws JcBaseException {
  // try {
  // JSONObject jweHeader = new JSONObject(jweHeaderStr);
  // checkHeader(jweHeader);
  // String jwtAlgStr = jweHeader.getString("alg");
  // if (!("ECDH-ES".equals(jwtAlgStr))) {
  // throw new NoSuchAlgorithmException("JWT algorithm: " + jwtAlgStr);
  // }
  //
  // JSONObject epkJson = jweHeader.getJSONObject("epk");
  // JSONArray jwkJSON = epkJson.getJSONArray("jwk");
  // JSONObject keyJSON = jwkJSON.getJSONObject(0);
  // // FIXME should there be a default or should this fail if no crv is
  // specified
  // String jwtCrvStr = keyJSON.optString("crv", "P-256");
  // String encodedX = keyJSON.getString("x");
  // String encodedY = keyJSON.getString("y");
  //
  // BigInteger x;
  // try {
  // x = new BigInteger(1, Base64.decodeUrl(encodedX));
  // } catch (Exception e) {
  // throw new JcBaseException("error decoding X", e);
  // }
  // BigInteger y;
  // try {
  // y = new BigInteger(1, Base64.decodeUrl(encodedY));
  // } catch (Exception e) {
  // throw new JcBaseException("error decoding Y", e);
  // }
  //
  // ASN1ObjectIdentifier oid = getNamedCurveOid(jwtCrvStr);
  // if (oid == null) {
  // throw new NoSuchAlgorithmException("JWT EC curve: " + jwtCrvStr);
  // }
  //
  // X9ECParameters x9ECParameters = SECNamedCurves.getByOID(oid);
  // ECCurve curve = x9ECParameters.getCurve();
  // ECPoint qB = curve.createPoint(x, y, false);
  // ECPoint q = new ECPoint.Fp(curve, qB.getX(), qB.getY());
  // ECDomainParameters ecDomainParameters = new ECDomainParameters(curve,
  // x9ECParameters.getG(),
  // x9ECParameters.getN(), x9ECParameters.getH(), x9ECParameters.getSeed());
  // ECPublicKeyParameters ecPublicKeyParameters = new
  // ECPublicKeyParameters(q,
  // ecDomainParameters);
  //
  // return encrypt(contentBytes, jweHeaderStr, ecPublicKeyParameters,
  // ecPrivateKeyParameters);
  // } catch (JSONException e) {
  // throw new JcBaseException(e);
  // } catch (NoSuchAlgorithmException e) {
  // throw new JcBaseException(e);
  // }
  // }

  private static Digest kdf2digest(String jwtKdfStr) throws NoSuchAlgorithmException {
    if ("CS256".equals(jwtKdfStr)) {
      return new SHA256Digest();
    }
    if ("CS384".equals(jwtKdfStr)) {
      return new SHA384Digest();
    }
    if ("CS512".equals(jwtKdfStr)) {
      return new SHA512Digest();
    }
    throw new NoSuchAlgorithmException("kdf " + jwtKdfStr);
  }

  /**
   * @param contentBytes
   * @param jwtHeaderStr
   *          alg = ENC_ALG_RSA1_5 or ENC_ALG_RSA_OAEP
   * @param rsaPublicKey
   *          recipient's public key
   * @return jwe
   * @throws JcBaseException
   */
  public static String encrypt(byte[] contentBytes, String jwtHeaderStr, RSAPublicKey rsaPublicKey,
      SecureRandom secureRandom) throws JcBaseException {
    try {
      JSONObject jweHeader = new JSONObject(jwtHeaderStr);
      checkHeader(jweHeader);
      String jwtEncStr = jweHeader.getString("enc");
      byte[] iv = new byte[16];
      // bits
      secureRandom.nextBytes(iv);
      String encodedIv = Base64.encodeBytes(iv, org.jsoncrypto.util.Base64.DONT_BREAK_LINES
          | org.jsoncrypto.util.Base64.URL);
      SecretKey contentEncryptionKey = generateRandomContentEncryptionKey(jwtEncStr, secureRandom);
      return encrypt(contentBytes, jwtHeaderStr, rsaPublicKey, contentEncryptionKey, encodedIv);
    } catch (JSONException e) {
      throw new JcBaseException(e);
    }
  }

  static SecretKey generateRandomContentEncryptionKey(String jwtEncStr, SecureRandom secureRandom)
      throws JcBaseException {
    int keylength = getEncKeyLength(jwtEncStr);
    return generateRandomContentEncryptionKey(keylength, secureRandom);
  }

  static SecretKey generateRandomContentEncryptionKey(int keylength, SecureRandom secureRandom) throws JcBaseException {
    SecretKey contentEncryptionKey;
    try {
      contentEncryptionKey = CryptoUtils.genAesKey(keylength, secureRandom);
    } catch (CryptoException e) {
      throw new JcBaseException(e);
    }
    return contentEncryptionKey;
  }

  static byte[] generateCIK(byte[] keyBytes, int cikByteLength, Digest kdfDigest, String encStr) throws JcBaseException {
    try {
      // "Integrity"
      final byte[] integrity = { 73, 110, 116, 101, 103, 114, 105, 116, 121 };
      int outputLengthInBits = cikByteLength * 8;
      byte[] encStrBytes = encStr.getBytes();
      ByteArrayOutputStream baos = new ByteArrayOutputStream(4 + encStrBytes.length + integrity.length);
      baos.write((byte) (outputLengthInBits >> 24));
      baos.write((byte) (outputLengthInBits >> 16));
      baos.write((byte) (outputLengthInBits >> 8));
      baos.write((byte) outputLengthInBits);
      baos.write(encStrBytes);
      baos.write(integrity);
      byte[] otherInfo = baos.toByteArray();
      KDFConcatGenerator kdfConcatGenerator = new KDFConcatGenerator(kdfDigest, otherInfo);
      kdfConcatGenerator.init(new KDFParameters(keyBytes, null));
      byte[] key = new byte[cikByteLength];
      kdfConcatGenerator.generateBytes(key, 0, key.length);
      return key;
    } catch (IOException e) {
      throw new JcBaseException(e);
    }
  }

  static byte[] generateCEK(byte[] keyBytes, int cekByteLength, Digest kdfDigest, String encStr) throws JcBaseException {
    try {
      final byte[] encryption = { 69, 110, 99, 114, 121, 112, 116, 105, 111, 110 };
      int outputLengthInBits = cekByteLength * 8;
      byte[] encStrBytes = encStr.getBytes();
      ByteArrayOutputStream baos = new ByteArrayOutputStream(4 + encStrBytes.length + encryption.length);
      baos.write((byte) (outputLengthInBits >> 24));
      baos.write((byte) (outputLengthInBits >> 16));
      baos.write((byte) (outputLengthInBits >> 8));
      baos.write((byte) outputLengthInBits);
      baos.write(encStrBytes);
      baos.write(encryption);
      byte[] otherInfo = baos.toByteArray();

      KDFConcatGenerator kdfConcatGenerator = new KDFConcatGenerator(kdfDigest, otherInfo);
      kdfConcatGenerator.init(new KDFParameters(keyBytes, null));
      byte[] key = new byte[cekByteLength];
      kdfConcatGenerator.generateBytes(key, 0, key.length);
      return key;
    } catch (IOException e) {
      throw new JcBaseException(e);
    }
  }

  public static int enc2cekLength(String jwtEncStr) throws NoSuchAlgorithmException {
    Integer cekLength = sEncAlgKeylengthMap.get(jwtEncStr);
    if (cekLength == null) {
      throw new NoSuchAlgorithmException("enc2cekLength enc=" + jwtEncStr);
    }
    return cekLength.intValue() / 8;
  }

  public static int int2cikLength(String jwtIntStr) throws NoSuchAlgorithmException {
    int cikLength;
    if (SIGN_ALG_HS256.equals(jwtIntStr)) {
      cikLength = 256 / 8;
    } else if (SIGN_ALG_HS384.equals(jwtIntStr)) {
      cikLength = 384 / 8;
    } else if (SIGN_ALG_HS512.equals(jwtIntStr)) {
      cikLength = 512 / 8;
    } else {
      throw new NoSuchAlgorithmException("WebToken RSA encrypt: CBC inc=" + jwtIntStr);
    }
    return cikLength;
  }

  /**
   * @param contentBytes
   * @param jweHeaderStr
   *          alg = ENC_ALG_RSA1_5 or ENC_ALG_RSA_OAEP
   * @param rsaPublicKey
   * @param contentEncryptionKey
   *          use the other function that randomly generates this key
   * @return
   * @throws JcBaseException
   */
  static String encrypt(byte[] contentBytes, String jweHeaderStr, RSAPublicKey rsaPublicKey,
      SecretKey contentEncryptionKey, String encodedIv) throws JcBaseException {
    try {
      JSONObject jweHeader = new JSONObject(jweHeaderStr);
      checkHeader(jweHeader);
      String jwtEncStr = jweHeader.getString("enc");

      String encodedJweEncryptedKey;

      String jwtAlgStr = jweHeader.getString("alg");
      if (KW_ALG_RSA1_5.equals(jwtAlgStr)) {
        encodedJweEncryptedKey = generateEncodedEncryptedKeyRSA15(rsaPublicKey, contentEncryptionKey);
      } else if (KW_ALG_RSA_OAEP.equals(jwtAlgStr)) {
        encodedJweEncryptedKey = generateEncodedEncryptedKeyRSAOAEP(rsaPublicKey, contentEncryptionKey);
      } else {
        throw new NoSuchAlgorithmException("JWT algorithm: " + jwtAlgStr);
      }

      return encryptAes(contentBytes, jweHeaderStr, jwtEncStr, contentEncryptionKey, encodedJweEncryptedKey, encodedIv);
    } catch (JSONException e) {
      throw new JcBaseException(e);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    }
  }

  static String generateEncodedEncryptedKeyRSAOAEP(RSAPublicKey rsaPublicKey, SecretKey contentEncryptionKey)
      throws JcBaseException {
    try {
      String encodedJweEncryptedKey;
      byte[] cipheredKeyBytes = CryptoUtils.rsaoaepEncryptBytes(contentEncryptionKey.getEncoded(), rsaPublicKey);
      encodedJweEncryptedKey = Base64.encodeBytes(cipheredKeyBytes, org.jsoncrypto.util.Base64.DONT_BREAK_LINES
          | org.jsoncrypto.util.Base64.URL);
      // System.out.println("jwtSymmetricKeySegment base64:" + b64);
      return encodedJweEncryptedKey;
    } catch (CryptoException e) {
      throw new JcBaseException(e);
    }
  }

  static String generateEncodedEncryptedKeyRSA15(RSAPublicKey rsaPublicKey, SecretKey contentEncryptionKey)
      throws JcBaseException {
    try {
      String encodedJweEncryptedKey;
      Cipher encrypter = Cipher.getInstance("RSA/ECB/PKCS1Padding");
      encrypter.init(Cipher.ENCRYPT_MODE, rsaPublicKey);
      byte[] ciphertext = encrypter.doFinal(contentEncryptionKey.getEncoded());
      encodedJweEncryptedKey = Base64.encodeBytes(ciphertext, org.jsoncrypto.util.Base64.DONT_BREAK_LINES
          | org.jsoncrypto.util.Base64.URL);
      return encodedJweEncryptedKey;
    } catch (BadPaddingException e) {
      throw new JcBaseException(e);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    } catch (NoSuchPaddingException e) {
      throw new JcBaseException(e);
    } catch (InvalidKeyException e) {
      throw new JcBaseException(e);
    } catch (IllegalBlockSizeException e) {
      throw new JcBaseException(e);
    }
  }

  static String encryptAesCbc(byte[] contentBytes, String jweHeaderStr, SecretKey contentEncryptionKey,
      String jwtEncStr, String encodedJweEncryptedKey, String encodedIv) throws JcBaseException {
    if (encodedIv == null) {
      throw new JcBaseException("encodedIv must not be null");
    }
    byte[] iv;
    try {
      iv = Base64.decodeUrl(encodedIv);
    } catch (Exception e) {
      throw new JcBaseException(e);
    }
    return encryptAesCbc(contentBytes, jweHeaderStr, contentEncryptionKey, jwtEncStr, encodedJweEncryptedKey, iv);
  }

  static String encryptAesCbc(byte[] contentBytes, String jweHeaderStr, SecretKey contentEncryptionKey,
      String jwtEncStr, String encodedJweEncryptedKey, SecureRandom sr) throws JcBaseException {
    byte[] iv = new byte[16];
    // bits
    sr.nextBytes(iv);
    return encryptAesCbc(contentBytes, jweHeaderStr, contentEncryptionKey, jwtEncStr, encodedJweEncryptedKey, iv);
  }

  static String encryptAesCbc(byte[] contentBytes, String jweHeaderStr, SecretKey contentEncryptionKey,
      String jwtEncStr, String encodedJweEncryptedKey, byte[] iv) throws JcBaseException {
    try {
      JSONObject jweHeader = new JSONObject(jweHeaderStr);

      String encodedIv = Base64.encodeBytes(iv, org.jsoncrypto.util.Base64.DONT_BREAK_LINES
          | org.jsoncrypto.util.Base64.URL);

      IvParameterSpec parameters = new IvParameterSpec(iv);
      String encodedJweHeader = Base64.encodeBytes(jweHeaderStr.getBytes("utf-8"),
          org.jsoncrypto.util.Base64.DONT_BREAK_LINES | org.jsoncrypto.util.Base64.URL);

      String encodedJweCiphertext;
      String encodedJweIntegrityValue;
      int cekLength = enc2cekLength(jwtEncStr);

      Digest kdfDigest = new SHA256Digest();
      byte[] cek = generateCEK(contentEncryptionKey.getEncoded(), cekLength, kdfDigest, jwtEncStr);

      byte[] plainbytes;
      String zip = jweHeader.optString("zip", null);
      if ("DEF".equals(zip)) {
        plainbytes = Compress.compress(contentBytes);
      } else {
        if (zip != null) {
          throw new JcBaseException("value of zip header MUST be DEF");
        } else {
          plainbytes = contentBytes;
        }
      }

      {
        System.out.println("base64 cek="
            + Base64.encodeBytes(cek, org.jsoncrypto.util.Base64.DONT_BREAK_LINES | org.jsoncrypto.util.Base64.URL));
        byte[] cipherTextBytes = CryptoUtils.jwtAesCbcEncrypt(plainbytes, cek, parameters);
        encodedJweCiphertext = Base64.encodeBytes(cipherTextBytes, org.jsoncrypto.util.Base64.DONT_BREAK_LINES
            | org.jsoncrypto.util.Base64.URL);
        {
          String stringToSign = encodedJweHeader + "." + encodedJweEncryptedKey + "." + encodedIv + "."
              + encodedJweCiphertext;
          String jwtIntStr = getIntAlg(jweHeader);
          Mac mac = getMac(jwtIntStr);
          int cikLength = mac.getMacLength();
          byte[] cik = generateCIK(contentEncryptionKey.getEncoded(), cikLength, kdfDigest, jwtEncStr);
          byte[] bytes = doMac(mac, cik, stringToSign.getBytes());
          encodedJweIntegrityValue = Base64.encodeBytes(bytes, org.jsoncrypto.util.Base64.DONT_BREAK_LINES
              | org.jsoncrypto.util.Base64.URL);
        }
      }
      StringBuffer sb = new StringBuffer(encodedJweHeader);
      sb.append('.');
      sb.append(encodedJweEncryptedKey);
      sb.append('.');
      sb.append(encodedIv);
      sb.append('.');
      sb.append(encodedJweCiphertext);
      sb.append('.');
      sb.append(encodedJweIntegrityValue);

      return sb.toString();
    } catch (JSONException e) {
      throw new JcBaseException(e);
    } catch (NoSuchAlgorithmException e) {
      throw new JcBaseException(e);
    } catch (CryptoException e) {
      throw new JcBaseException(e);
    } catch (UnsupportedEncodingException e) {
      throw new JcBaseException(e);
    } catch (Exception e) {
      throw new JcBaseException(e);
    }
  }

  /**
   * @param contentBytes
   * @param jweHeaderStr
   *          if this does not specify IV it will be generated
   * @param contentEncryptionKey
   * @param encodedJweEncryptedKey
   * @return jwe
   * @throws JSONException
   * @throws UnsupportedEncodingException
   * @throws JcBaseException
   * @throws InvalidCipherTextException
   */
  static String encryptAesGcm(byte[] contentBytes, String jweHeaderStr, SecretKey contentEncryptionKey,
      String encodedJweEncryptedKey, String encodedIv) throws JcBaseException {
    try {
      String encodedJweHeader = Base64.encodeBytes(jweHeaderStr.getBytes("utf-8"),
          org.jsoncrypto.util.Base64.DONT_BREAK_LINES | org.jsoncrypto.util.Base64.URL);

      String encodedJweCiphertext;
      String encodedJweIntegrityValue;
      byte[] ivBytes;
      JSONObject header = new JSONObject(jweHeaderStr);
      ivBytes = Base64.decodeUrl(encodedIv);

      byte[] plainbytes;
      String zip = header.optString("zip", null);
      if ("DEF".equals(zip)) {
        plainbytes = Compress.compress(contentBytes);
      } else {
        if (zip != null) {
          throw new JcBaseException("value of zip header MUST be DEF");
        } else {
          plainbytes = contentBytes;
        }
      }

      String associatedText = encodedJweHeader.concat(".").concat(encodedJweEncryptedKey).concat(".").concat(encodedIv);
      byte[] associatedTextBytes = associatedText.getBytes();

      System.out.println("contentEncryptionKey bitlength = " + (contentEncryptionKey.getEncoded().length * 8));
      // String[] result = CryptoUtils.aesgcmEncrypt(aeadParameters,
      // contentEncryptionKey, mJsonStr.getBytes("utf-8"));
      String[] result = CryptoUtils.aesgcmEncrypt(contentEncryptionKey.getEncoded(), plainbytes, ivBytes,
          associatedTextBytes);
      encodedJweCiphertext = result[0];
      encodedJweIntegrityValue = result[1];

      StringBuffer sb = new StringBuffer(encodedJweHeader);
      sb.append('.');
      sb.append(encodedJweEncryptedKey);
      sb.append('.');
      sb.append(encodedIv);
      sb.append('.');
      sb.append(encodedJweCiphertext);
      sb.append('.');
      sb.append(encodedJweIntegrityValue);

      return sb.toString();
    } catch (JSONException e) {
      throw new JcBaseException(e);
    } catch (IllegalStateException e) {
      throw new JcBaseException(e);
    } catch (InvalidCipherTextException e) {
      throw new JcBaseException(e);
    } catch (Exception e) {
      throw new JcBaseException(e);
    }
  }

  static public byte[] decrypt(String jwtStr, RSAPrivateKey aRsaPrivKey) throws JcBaseException {
    try {
      String[] split = jwtStr.split("\\.");
      if (split.length != 5) {
        throw new JcBaseException("jwt format exception #" + split.length);
      }

      String encodedJwtHeaderSegment = split[0];
      String encodedJwtKeySegment = split[1];
      String encodedJwtIvSegment = split[2];
      String encodedJwtCryptoSegment = split[3];
      String encodedJwtIntegritySegment = split[4];
      byte[] jwtIntegritySegmentBytes = Base64.decodeUrl(encodedJwtIntegritySegment);
      String jwtHeaderStr = new String(Base64.decodeUrl(encodedJwtHeaderSegment));
      JSONObject header = new JSONObject(jwtHeaderStr);

      String newKeySegment = split[1];
      byte[] cipheredKeyBytes = Base64.decodeUrl(newKeySegment);

      String jwtAlgStr = header.getString("alg");
      byte[] secretKeyBytes = decryptKey(aRsaPrivKey, jwtAlgStr, cipheredKeyBytes);

      return doJwtDecrypt(encodedJwtHeaderSegment, encodedJwtKeySegment, encodedJwtCryptoSegment,
          jwtIntegritySegmentBytes, header, secretKeyBytes, encodedJwtIvSegment);
    } catch (JSONException e) {
      throw new JcBaseException(e);
    } catch (Exception e) {
      throw new JcBaseException(e);
    }
  }

  static String getSymmetricAlgorithm(String jwtEncStr) throws NoSuchAlgorithmException {
    return "AES";
  }

  static byte[] decryptKey(RSAPrivateKey rsaPrivateKey, String jwtAlgStr, byte[] cipheredKeyBytes)
      throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException,
      BadPaddingException, CryptoException {
    if (KW_ALG_RSA1_5.equals(jwtAlgStr)) {
      Cipher encrypter = Cipher.getInstance("RSA/ECB/PKCS1Padding");
      encrypter.init(Cipher.DECRYPT_MODE, rsaPrivateKey);
      return encrypter.doFinal(cipheredKeyBytes);
    } else if (KW_ALG_RSA_OAEP.equals(jwtAlgStr)) {
      return CryptoUtils.decryptRSAOAEP(cipheredKeyBytes, rsaPrivateKey);
    } else {
      throw new NoSuchAlgorithmException("RSA decrypt " + jwtAlgStr);
    }
  }
  
  static public JSONObject toJWK(RSAPublicKey rsaPublicKey, String kid)
      throws JSONException, InvalidKeyException {
    JSONObject jwk = new JSONObject();
    jwk.put("kty", "RSA");
    jwk.put("e",
        Base64.encodeBytes(rsaPublicKey.getPublicExponent().toByteArray(), Base64.DONT_BREAK_LINES | Base64.URL));
    BigInteger modulus = rsaPublicKey.getModulus();
    int bitLength = rsaPublicKey.getModulus().bitLength();
    byte[] modulusBytes = modulus.toByteArray();
    if (modulusBytes.length == bitLength / 8) {
      jwk.put("n", Base64.encodeBytes(modulusBytes,
        Base64.DONT_BREAK_LINES | Base64.URL));
    } else {
      if (modulusBytes.length-1 == bitLength / 8) {
        // leading zero byte
        jwk.put("n", Base64.encodeBytes(modulusBytes, 1, modulusBytes.length-1,
          Base64.DONT_BREAK_LINES | Base64.URL));
      } else {
        // ooops
        throw new InvalidKeyException("strange modulus twos complement length: " + modulusBytes.length);
      }
    }
    if (kid != null) {
      jwk.put("kid", kid);
    }
    return jwk;
  }

  static public RSAPublicKey fromJWK(JSONObject jwk, RSAPublicKey dummy) throws Exception {
    String kty = jwk.getString("kty");
    if (!"RSA".equals(kty)) {
      throw new IllegalArgumentException("jwk is not a RSA public key");
    }
    String nStr = jwk.getString("n");
    String eStr = jwk.getString("e");

    BigInteger N = new BigInteger(1, Base64.decodeUrl(nStr));
    BigInteger E = new BigInteger(1, Base64.decodeUrl(eStr));
    RSAPublicKeySpec keySpec = new RSAPublicKeySpec(N, E);
    RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
        .generatePublic(keySpec);
    return pubKey;
  }
  
  static public ECPublicKeyParameters fromJWK(JSONObject jwk, ECPublicKeyParameters dummy) throws Exception {
    String kty = jwk.getString("kty");
    if (!"EC".equals(kty)) {
      throw new IllegalArgumentException("jwk is not a EC public key");
    }
    String curve;
    String crv = jwk.getString("crv");
    if ("P-256".equals(crv)) {
      curve = "secp256r1";
    } else if ("P-384".equals(crv)) {
      curve = "secp384r1";
    } else if ("P-521".equals(crv)) {
      curve = "secp521r1";
    } else {
      throw new IllegalArgumentException("unsupported crv");
    }

    String xStr = jwk.getString("x");
    String yStr = jwk.getString("y");

    BigInteger x = new BigInteger(1, Base64.decodeUrl(xStr));
    BigInteger y = new BigInteger(1, Base64.decodeUrl(yStr));
    
    ECPublicKeyParameters ecPublicKeyParameters = JcBase.getecPublicKeyParameters(curve, x, y);
    return ecPublicKeyParameters;
  }
  
}
